ε Administración de la memoria bajo DOS (I) En este artículo se pretende mostrar las distintas formas de administrar la memoria en el sistema operativo DOS ayudandonos de rutinas de ejemplo y pequeños programas. Se ha dividido en 2 el artículo, explicando en el primero los conceptos básicos, la memoria convencional y la XMS y dejando para el segundo (para el próximo número de la Phy) la memoria EMS, el modo flat y una pequeña introducción al modo protegido. El nivel de los artículos será medio ya que hará falta conocer el lenguaje ensamblador y dominar los conceptos de interrupciones y segmentos. φ La memoria convencional La llamada memoria convencional o memoria baja es una zona de memoria más bien escasa en estos dias donde, bajo DOS, se cargan los programas y se les asignan recursos. Mide exactamente 640Kb (cifra odiada por muchos) y se divide en segmentos de 64Kb con una granularidad de 16 bytes (para saber más sobre esto, ver sección de programación en ensamblador). Para poder pedir memoria dinámicamente al DOS en ensamblador, lo primero que hay que hacer es liberar la memoria que el DOS nos ha asignado y que no vamos a utilizar. El sistema operativo, al cargar todo programa COM o EXE, le da toda la memoria libre que resta hasta la zona ocupada por el própio sistema y la BIOS (segmento 0A000h) con lo que cualquier petición de memoria que le realizemos (a no ser que sea muy pequeña) nos devolverá un precioso error de memoria insuficiente. Bien, ya sabemos que tenemos que liberar la memoria no ocupada, pero ¿como saber que memoria es la que estoy ocupando? Sencillo, el tamaño estrictamente necesario es el que mide el PSP (siempre 256 bytes), el del código y el de la pila. Si recordamos todo eso de la disposición de la pila, el segmento de código y el PSP, pues podemos escribir una función para liberar la memoria en un fichero EXE o COM muy facilmente. Como ejemplo aquí está la que libera la memoria de un EXE, luego esplicaré que hace la función δ4Ahπ de la INT 21h: ΓLiberaMEM PROC Ω ; Función que libera la memoria Γ MOV BX, SS Ω ; reservada por el DOS a nuestro Γ MOV AX, ES Ω ; programa y que no es utilizada. Γ SUB BX, AX Ω ; ES debe contener el PSP. Γ MOV AX, SP Γ ADD AX, 15d Γ SHR AX, 4 Γ ADD BX, AX Γ MOV AH, 4Ah Γ INT 21h Γ RET ΓLiberaMEM ENDP En los lenguajes de alto nivel como el C o Pascal, la cosa cambia; a priori no sabemos si el runtime del compilador ha liberado esta memoria o no ya que esto depende del fabricante del lenguaje o, más aún, de las funciones que se vayan a usar durante la ejecución del programa. Teniendo esto en cuenta y siempre que nos sea posible, se deberia dejar al propio lenguaje que adminis- trara la memoria y pedirsele mediante las llamadas estandar y no desde el assembler, aunque siempre podemos arriesgarnos o debuggear el runtime para ver lo que hace. En concreto, por ejemplo, el Turbo Pascal 6.0, no libera la memo- ria y la usa como le parece concediendonos recursos cuando se los pedimos y li- berandolos automáticamente al finalizar. El API del DOS se proviene de unas cuantas funciones para la administración de la memoria entre las que destacan la δ48hπ ("allocate memory"), la δ49hπ ("free memory") y la δ4Ahπ ("modify memory block"). La estructura interna que usa el DOS para hacer estas operaciones, queda fuera del alcance de este artículo, pero si alguien está interesado, puede buscar en libros especializa- dos como por ejemplo el "φPC Internoπ", documentos o FAQs sobre los MCBs ("memory control block") que es el nombre de estas estructuras (otra buena fuente de información sobre esto son los virus, sobre los que precisamente hay un par de articulo en cada número de la Phy). Ω Interrupción: 21h Función: 48h "Allocate memory" Σ Parámetros: AH = 48h Σ BX = Párrafos que se necesita Σ Devuelve: Si carry -> AX = Código de error Σ BX = Tamaño máximo permitido Σ Sino -> AX = Segmento disponible. A esta función se le debe pasar en BX el número de párrafos de memoria que se necesitan y devuelve un segmento donde a partir de la dirección 0000, se puede usar libremente sin miedo (teoricamente) a que te lo pisen o a cargarte el sistema. Los párrafos de memoria son una medida de cantidad que equivale a 16 bytes, por lo que si necesitamos 60000 bytes, hemos de pedir 10000 párrafos, sencillo, ¿no? Es importante que en el segmento que nos concederá el sistema, no escribamos más allá de lo que estrictamente hemos pedido o nos cargaremos el MCB siguiente con lo que el sistema se colgará. El código de error que nos devuelve es 7 o 8, el primero significa que algun MCB está destruido y el otro que no queda memoria libre. Ω Interrupción: 21h Función: 49h "Free allocated memory" Σ Parámetros: AH = 49h Σ ES = Segmento reservado a liberar Σ Devuelve: Si carry -> AX = Código de error Esta función, simplemente devuelve la memoria que habiamos pedido con la interrupción anterior. En ES debemos poner lo que habia devuelto AX antes y si el carry está a 1 en AX podremos tener un código de error 7 o 9, que significan que los MCBs estan destruidos o que el segmento no es válido respectivamente. Ω Interrupción: 21h Función: 4Ah "Modify memory allocation" Σ Parámetros: AH = 4Ah Σ BX = Párrafos que se necesita Σ ES = Segmento a modificar Σ Devuelve: Si carry -> AX = Código de error Σ BX = Tamaño máximo permitido Con esta función podemos modificar el tamaño de un determinado bloque de memoria asignado como hemos hecho, por ejemplo para la función que liberaba la memoria no usada por el programa. En BX pasamos el número de párrafos que queremos y en ES el segmento que nos devolvieron en una llamada anterior (en el caso de liberar la memoria asignada por el DOS, el segmento del PSP que es el primero que nos dan). Los códigos de error que nos devuelve son 7, 8 o 9 que significan respectivamente MCBs destruidos, memoria insuficiente y segmento no válido. Para ilustrar esta forma de administrar la memoria teneis en el directorio 'φMEMORYπ' un par de programas llamados 'memcon1.asm' y 'memcon2.asm' que mues- tran lo que se puede y debe hacer y lo que no. El primero pide memoria y luego la devuelve antes de salir sin problemas; el segundo pide memoria y escribe en ella más bytes de los pedidos con lo que al tratar de volver shell del sistema, hay muchas posibilidades de no poder hacerlo. φ La memoria XMS Se le llama así a la memoria que reside por encima de 1Mb y que en el diseño original del 8086 no exisitia. Como los compatibles IBM fueron evolucionando y las aplicaciones comenzaron a pedir más y más recursos, al 80286 se le proporcionó un bus de direcciones un poco más grande con lo que se le dotó de hasta 16Mb direccionables. Con ello surgia un problema de compatibilidad que impedia que los 15 nuevos megas se manejaran como antes se hacia con la memoria convencional y ello hizo aparecer en escena el estandar EMS de la mano de IBM, Lotus y Microsoft (tm, todos). Pero esto no nos interesa en este número almenos, lo importante es que la EMS entraba al ordenador mediante hardware y eso no era una gran idea y por ello se evolucionó con la llegada de los 80386 y el modo protegido (mejor que el del 80286) hacia un estandar llamado XMS que entraba mediante un driver con lo cual se hacia más cómodo. El driver del que hablo es el 'EMM386.EXE' que seguro tienes instalado en tu PC ya que tambien se encarga de simular memoria EMS y por tanto es casi esencial en todos los programas que usen más de medio mega de memoria. Su funcionamiento interno lo "intuiremos" en el próximo número cuando hablemos del modo protegido y el modo flat ya que no dista mucho de lo que haremos allí. Para usar la memoria XMS lo primero que hay que hacer es comprobar que el driver 'EMM386.EXE' ha sido cargado correctamente. Para ello, tenemos una función del multiplexor (INT 2Fh) que nos devolverá información sobre si está o no instalado y sobre la dirección del driver en caso afirmativo. Para saber si está en memoria el driver llamaremos a la INT 2Fh con el valor 4300h en AX y si al regresar, en AL tenemos el valor 80h es que el driver se encuentra en memoria y por tanto podemos preguntar por su dirección. Para hacer esto último, llamaremos con AX=4310h a la INT 2Fh y nos devolverá en ES:BX la dirección don- de se encuentra el driver instalado. A partir de este momento, nos olvidamos de las interrupciones y todo lo que haremos será llamar al driver (con CALL FAR) dejándole el número de función que queremos que haga en AH como si fuera una interrupción cualquiera. La lista de las funciones del driver es esta: Ω 00 --> Γ "Get XMS version number" Ω 01 --> Γ "Request High Memory Area" Ω 02 --> Γ "Release High Memory Area" Ω 03 --> Γ "Global enable A20, for using the HMA" Ω 04 --> Γ "Global disable A20" Ω 05 --> Γ "Local enable A20" Ω 06 --> Γ "Local disable A20" Ω 07 --> Γ "Query A20 state" Ω 08 --> Γ "Query free extended memory" Ω 09 --> Γ "Allocate extended memory block" Ω 0A --> Γ "Free extended memory block" Ω 0B --> Γ "Move extended memory block" Ω 0C --> Γ "Lock extended memory block" Ω 0D --> Γ "Unlock extended memory block" Ω 0E --> Γ "Get handle information" Ω 0F --> Γ "Reallocate extended memory block" Ω 10 --> Γ "Request upper memory block" Ω 11 --> Γ "Release upper memory block" Ω 12 --> Γ "Reallocate upper memory block" (v3.0+) Ω 88 --> Γ "Query free extended memory" (v3.0+) Ω 89 --> Γ "Allocate any extended memory" (v3.0+) Ω 8E --> Γ "Get extended EMB handle information" (v3.0+) Ω 8F --> Γ "Reallocate any extended memory block" (v3.0+) En realidad de todas estas funciones usaremos sólo unas pocas como la 09h, la 0Ah y la 0Bh, ya que todas las demás desde el punto de vista que aquí se está tratando son prescindibles. De todos modos, es muy interesante saber para que sirven y como se usan ya que gracias a algunas de ellas se puede acceder a los UMBs (para instalar residentes), cambiar el estado de la línea A20, etc. La función 09h se encarga de pedir memoria al driver tal y como lo haciamos con la 48h de la INT 21h. A esta función le pasaremos en DX el número de Kb (ojo, 1Kb=1024 bytes) que deseamos y nos devolverá un handle de 16bits en DX que deberemos guardar. La condición de error nos la dará en AX, si este regis- tro es 0, hay error y debemos mirar en BL el código de error devuelto (luego hablaré sobre estos códigos). La función 0Ah es tan simple como la anterior, se le pasa en DX el handle de memoria que se quiere liberar y ya está. En AX dirá si hay o no algún error, si AX vale 0 es que ha ocurrido algo inesperado y en BL se encontrará el código de error correspondiente. La cosa se complica un poco con la función 0Bh que es la encargada de mover bloques de memoria entre RAM convencional y alta. No se le pasan los parámetros en los registros ni en la pila, sino en una estructura especial llamada EMM que apuntaremos antes de llamar con DS:SI. Como siempre, si al regresar AX es 0 es que ha ocurrido algun error y en cuyo caso tendremos en BL el código de error. La estructura EMM se compone de 3 dobles WORDs y 2 WORDs dispuestas de la siguiente forma: Γ Longitud DD ? Γ SHandle DW ? Γ SOffset DD ? Γ DHandle DW ? Γ DOffset DD ? El parámetro 'Logitud' debe contener la longitud en bytes del bloque a copiar y que dadas las limitaciones del DOS y el modo real no deberia de pasar de 64Kb, aunque hay trucos enrevesados para poder pasar de este tamaño. Los pará- metros 'SHandle' y 'DHandle' ("source handle" y "destination handle") son los handles de XMS a usar, es decir, los que devuelve la función 09h. Podrias pen- sar ahora que entonces no se puede copiar entre memoria convencional y memoria XMS ya que los handles devueltos por el driver se refieren a la memoria por en- cima de 1Mb y... pues no es del todo correcto ya que cuando quieras mover blo- ques de memoria entre la convencional y la XMS, simplemente debes poner como handle de la convencional el 0. Por último, los parámetros 'SOffset' y 'DOffset' son el offset a partir del cual copiar; en XMS se refiere al despla- zamiento dentro del hipotético segmento reservado y en memoria baja hay que po- ner la dirección con segmento y offset hasta la que copiar o desde la cual se copia. Para ilustrar todo esto he preparado una pequeña libreria de funciones que te permitiran usar desde el ensamblador toda la memoria XMS. El código fuente está en el fichero 'δXMSMEM.INCπ' y ha sido comentado a conciencia con lo que supongo que no habrá problema alguno en entenderlo todo y si es necesario en hacerle los prototipos para incluirlo en un lenguaje de alto nivel. De todos modos, si no sabes mucho ensamblador y quieres manejar la memoria XMS desde tu lenguaje favorito, enviame un mail y para el próximo número incluiré una ver- sión para el lenguaje que me pidas. Además de esto, en el directorio 'σMEMORYπ' hay un fichero llamado 'δXMSDRV.TXTπ' que ha sido extraido de la lista de inte- rrupciones de φRalf Brown (v5.3)π y que detalla todas las funciones del driver EMM386 i los códigos de error correspondientes. ∞ Navi/PhyMosys